/+
Library dbapi - Simple database api for the D programming language.

Copyright (C) 2007 Myron Alexander (myron.alexander...gmail.com)

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+/

module dbapi.internal.sqlite3.transaction;

private import std.string;

private import dbapi.base;
private import dbapi.internal.sqlite3.error;
private import dbapi.internal.sqlite3.internalconnection;
private import dbapi.internal.sqlite3.definitions;

private import sqlite.sqlite3;

/**
 * A statement wrapper for the transaction statements BEGIN, COMMIT, ROLLBACK.
 *
 * There is a special case for COMMIT where the statement will return a
 * non-zero return code but must not
 *
 */
private class TransactionStatement {

   this (sqlite3* dbcon, char[] text)
   in {
      assert (dbcon !is null,
         "TransactionStatement: dbcon is null, perhaps connection not created?");
      assert (text !is null,
         "TransactionStatement: statement text is null.");
      assert (text.length > 0,
         "TransactionStatement: statement text is empty string.");
   }
   body {
      int rc =
         sqlite3_prepare_v2 (
            dbcon, toStringz (text), text.length, &m_stmt, null);

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException)(
            dbcon,
            "Failed to create transaction statement")
            .setSql (text);
      }

      m_text  = text;
      m_dbcon = dbcon;
   }

   ~this() {
      debug {
         writefln ("Transaction statement destructing");
      }

      int rc = sqlite3_finalize (m_stmt);

      switch (rc) {
         case SQLITE_OK:
         case SQLITE_ABORT:
            // Closing is done after the block.
            break;

         default:
            // Do not raise an exception. I removed the exception as the
            // finalize may be called after another error. In that case, I do
            // not want that error to be masked by this one. It is very unlikely
            // that the finalize will fail but if it does, it should be added to
            // a log, or a set of warnings.

            // TODO: Add the failure to a log or a set of warnings.

            //~ throw SqliteError!(SqlOperationalException) (
               //~ m_dbcon,
               //~ "Error on closing transaction statement. "
               //~ "Sqlite error ($(errmsg))."
               //~ "SQL (" ~ m_text ~ ").");
      }
   }

   /**
    * Execute the transaction statement.
    *
    * When the transaction statement is COMMIT, a SQLITE_BUSY code may be
    * returned. In that case, the method will return false.
    *
    * Returns:
    *   true when the statement executed without problems.
    *   false when the statement could not execute due to locked database.
    */
   bool exec () {
      int rc = sqlite3_reset (m_stmt);

      if (SQLITE_OK == rc) {
         rc = sqlite3_step (m_stmt);

         switch (rc) {
            case SQLITE_BUSY:
               return false;

            case SQLITE_DONE:
               rc = SQLITE_OK;

            default:
               // Do nothing to allow the error handler below to handle the
               // error.
         }
      }

      debug {
         writefln ("%X - %s: RC = %d", &this, m_text, rc);
      }

      if (SQLITE_OK != rc) {
         throw SqliteError!(SqlOperationalException)(
            m_dbcon,
            "Failed to execute transaction statement")
            .setSql (m_text);
      }

      return true;
   }

   private sqlite3_stmt*   m_stmt;
   private char[]          m_text;
   private sqlite3*        m_dbcon;
}

interface TransactionModeHandlerFactory {
   TransactionModeHandler createTransactionModeHandler (
                                            sqlite3*        dbcon
                                          , TransactionMode transactionMode
                                          , TransactionType transactionType );
}



class StdTrMoHaFactory : TransactionModeHandlerFactory {

   override TransactionModeHandler createTransactionModeHandler (
                                            sqlite3*        dbcon
                                          , TransactionMode transactionMode
                                          , TransactionType transactionType ) {
      switch (transactionMode) {

         case TransactionMode.USER:
            return new UserModeHandler (
                                    dbcon, transactionMode, transactionType);

         case TransactionMode.AUTO_COMMIT:
            return new AutoCommitModeHandler (
                                    dbcon, transactionMode, transactionType);

         case TransactionMode.ON_MODIFY:
            return new OnModifyModeHandler (
                                    dbcon, transactionMode, transactionType);

         case TransactionMode.ALWAYS:
            return new AlwaysModeHandler (
                                    dbcon, transactionMode, transactionType);

         default:
            throw (new SqlInterfaceException ("Unknown Transaction Mode"))
               .setLongMsg (
                  "Failed to create transaction mode handler as the "
                  "transaction mode ($(TransactionMode)) is unknown. This is "
                  "an internal error. Please log a bug report.")
               .setProperty ("TransactionMode", transactionMode);
      }
   }
}




private bool inTransactionImpl (sqlite3* dbcon) {
   /* In C, TRUE <> 0, FALSE == 0. The sqlite3_get_autocommit returns FALSE
    * when a transaction is in effect.
    */
   return sqlite3_get_autocommit (dbcon) == 0;
}



/**
 * Implements the handling of USER transaction mode.
 *
 * The handler does nothing as, by definition of USER transaction mode, the
 * user assumes the responsibility for transaction control.
 */
class UserModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (  sqlite3*           dbcon
         , TransactionMode    transactionMode
         , TransactionType    transactionType) {

      debug {
         writefln ("UserModeHandler instantiating");
      }

      m_dbcon           = dbcon;
      m_transactionMode = transactionMode;
      m_transactionType = transactionType;
   };

   ~this () {
      debug {
         writefln ("UserModeHandler destructing");
      }
   }

   //
   // Statement creation events
   //

   void validateStatementType (StatementType statementType) {
   }

   //
   // Batch execution events
   //

   override void startBatch () {
   }

   override void batchError () {
   }

   override void endBatch () {
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType) {
   }

   override void executionError () {
   }

   override void endExecute () {
   }

   //
   // A database operation was aborted. Transaction may have been terminated.
   //

   override void onAbort () {
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit () {
      return true;
   }

   override void rollback () {
   }

   override void onClose () {
   }
   
   //
   // Transaction state
   //

   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }

   //
   //
   //

   override TransactionMode transactionMode () {
      return m_transactionMode;
   }

   override TransactionType transactionType () {
      return m_transactionType;
   }



   protected sqlite3*         m_dbcon;
   protected TransactionMode  m_transactionMode;
   protected TransactionType  m_transactionType;
}

class AutoCommitModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (  sqlite3*           dbcon
         , TransactionMode    transactionMode
         , TransactionType    transactionType) {

      debug {
         writefln ("AutoCommitModeHandler instantiating");
      }

      m_dbcon           = dbcon;
      m_transactionMode = transactionMode;
      m_transactionType = transactionType;

      m_commitStmt = new TransactionStatement (m_dbcon, "commit");
      scope(failure) delete m_commitStmt;

      m_beginStmt = new TransactionStatement (m_dbcon, "begin");
      scope(failure) delete m_beginStmt;
   };

   ~this () {
      debug {
         writefln ("AutoCommitModeHandler destructing");
      }
      delete m_commitStmt;
      delete m_beginStmt;
   }

   //
   // Statement creation events
   //

   void validateStatementType (StatementType statementType) {
      if (StatementType.STATEMENT_TRANSACTION == statementType) {
         throw (new SqlInterfaceException (
            "Transaction statement invalid for this mode"))
            .setLongMsg (
               "Invalid statement. A transaction statement (BEGIN, COMMIT, "
               "ROLLBACK) is not valid for this transaction mode.")
            .setProperty ("TransactionModeHandler", "AutoCommitModeHandler")
            .setProperty ("StatementType", statementType);
      }
   }

   //
   // Batch execution events
   //

   override void startBatch ()
   in {
      assert (!inTransaction);
   }
   body {
      m_beginStmt.exec ();
   }

   override void batchError () {
      if (inTransaction) {
         m_commitStmt.exec ();
      }
   }

   override void endBatch ()
   in {
      assert (inTransaction);
   }
   body {
      m_commitStmt.exec ();
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType) {
   }

   override void executionError () {
   }

   override void endExecute () {
   }

   //
   // A database operation was aborted. Transaction may have been terminated.
   //

   override void onAbort () {
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit () {
      return true;
   }

   override void rollback () {
   }

   override void onClose () {
   }
   
   //
   // Transaction state
   //

   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }

   //
   //
   //

   override TransactionMode transactionMode () {
      return m_transactionMode;
   }

   override TransactionType transactionType () {
      return m_transactionType;
   }



   protected sqlite3*               m_dbcon;
   protected TransactionStatement   m_commitStmt;
   protected TransactionStatement   m_beginStmt;
   protected TransactionMode        m_transactionMode;
   protected TransactionType        m_transactionType;
}


class OnModifyModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (  sqlite3*           dbcon
         , TransactionMode    transactionMode
         , TransactionType    transactionType) {

      debug {
         writefln ("OnModifyModeHandler instantiating");
      }

      m_dbcon           = dbcon;
      m_transactionMode = transactionMode;
      m_transactionType = transactionType;

      m_commitStmt = new TransactionStatement (m_dbcon, "commit");
      scope(failure) delete m_commitStmt;

      m_rollbackStmt = new TransactionStatement (m_dbcon, "rollback");
      scope(failure) delete m_rollbackStmt;

      switch (transactionType) {
         case TransactionType.DEFAULT:
            m_beginStmt = new TransactionStatement (m_dbcon, "begin");
            break;

         case TransactionType.DEFERRED:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin deferred");
            break;

         case TransactionType.IMMEDIATE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin immediate");
            break;

         case TransactionType.EXCLUSIVE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin exclusive");
            break;

         default:
            throw (new SqlInterfaceException (
               "Invalid transaction type for mode"))
               .setLongMsg (
                  "Invalid transaction type ($(TransactionType)) for mode "
                  "($(TransactionModeHandler)).")
               .setProperty ("TransactionModeHandler", "OnModifyModeHandler")
               .setProperty ("TransactionType", transactionType);
      }
      
      scope(failure) delete m_beginStmt;
   };

   ~this () {
      debug {
         writefln ("OnModifyModeHandler destructing");
      }

      delete m_commitStmt;
      delete m_rollbackStmt;
      delete m_beginStmt;
   }

   //
   // Statement creation events
   //

   void validateStatementType (StatementType statementType) {
      if (StatementType.STATEMENT_TRANSACTION == statementType) {
         throw (new SqlInterfaceException (
            "Invalid statement. A transaction statement (BEGIN, COMMIT, "
            "ROLLBACK) is not valid for this transaction mode."))
            .setProperty ("TransactionModeHandler", "OnModifyModeHandler")
            .setProperty ("StatementType", statementType);
      }
   }

   //
   // Batch execution events
   //

   override void startBatch () {
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   override void batchError () {
   }

   override void endBatch () {
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType) {

      switch (statementType) {

         case StatementType.STATEMENT_MODIFY:
            if (!inTransaction) {
               m_beginStmt.exec ();
            }
            break;

         /* When the statement is other than a DQL, or DML statement, the
          * transaction must be committed to ensure consistent operation.
          * Example of 'other' statements are DDL, and pragma.
          */
         case StatementType.STATEMENT_OTHER:
            if (inTransaction) {
               m_commitStmt.exec ();
            }
            break;

         default:
            // Do nothing for other types
      }
   }

   override void executionError () {
   }

   override void endExecute () {
   }

   //
   // A database operation was aborted. Transaction may have been terminated.
   //

   override void onAbort () {
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit () {
      if (inTransaction) {
         return m_commitStmt.exec ();
      } else {
         return true;
      }
   }

   override void rollback () {
      if (inTransaction) {
         m_rollbackStmt.exec ();
      }
   }

   override void onClose () {
      if (inTransaction) {
         m_rollbackStmt.exec ();
      }
   }

   //
   // Transaction state
   //

   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }

   //
   //
   //

   override TransactionMode transactionMode () {
      return m_transactionMode;
   }

   override TransactionType transactionType () {
      return m_transactionType;
   }



   protected sqlite3*               m_dbcon;
   protected TransactionStatement   m_commitStmt;
   protected TransactionStatement   m_beginStmt;
   protected TransactionStatement   m_rollbackStmt;
   protected TransactionMode        m_transactionMode;
   protected TransactionType        m_transactionType;
}

class AlwaysModeHandler : TransactionModeHandler {

   //
   // Connection level events
   //

   this (  sqlite3*           dbcon
         , TransactionMode    transactionMode
         , TransactionType    transactionType) {

      debug {
         writefln ("AlwaysModeHandler instantiating");
      }

      m_dbcon           = dbcon;
      m_transactionMode = transactionMode;
      m_transactionType = transactionType;

      m_commitStmt = new TransactionStatement (m_dbcon, "commit");
      scope(failure) delete m_commitStmt;

      m_rollbackStmt = new TransactionStatement (m_dbcon, "rollback");
      scope(failure) delete m_rollbackStmt;

      switch (transactionType) {
         case TransactionType.DEFAULT:
            m_beginStmt = new TransactionStatement (m_dbcon, "begin");
            break;

         case TransactionType.DEFERRED:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin deferred");
            break;

         case TransactionType.IMMEDIATE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin immediate");
            break;

         case TransactionType.EXCLUSIVE:
            m_beginStmt =
               new TransactionStatement (m_dbcon, "begin exclusive");
            break;

         default:
            throw (new SqlInterfaceException (
               "Invalid transaction type for mode"))
               .setLongMsg (
                  "Invalid transaction type ($(TransactionType)) for mode "
                  "($(TransactionModeHandler)).")
               .setProperty ("TransactionModeHandler", "AlwaysModeHandler")
               .setProperty ("TransactionType", transactionType);
      }

      scope(failure) delete m_beginStmt;

      m_beginStmt.exec ();
   };

   ~this () {
      debug {
         writefln ("AlwaysModeHandler destructing");
      }

      delete m_commitStmt;
      delete m_rollbackStmt;
      delete m_beginStmt;
   }

   //
   // Statement creation events
   //

   void validateStatementType (StatementType statementType) {
      if (StatementType.STATEMENT_TRANSACTION == statementType) {
         throw (new SqlInterfaceException (
            "Invalid statement for transaction mode"))
            .setLongMsg (
               "Invalid statement. A transaction statement (BEGIN, COMMIT, "
               "ROLLBACK) is not valid for this transaction mode "
               "$(TransactionModeHandler).")
            .setProperty ("TransactionModeHandler", "AlwaysModeHandler")
            .setProperty ("StatementType", statementType);
      }
   }

   //
   // Batch execution events
   //

   override void startBatch ()
   in {
      assert (inTransaction);
   }
   body {
   }

   override void batchError ()
   out {
      assert (inTransaction);
   }
   body {
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   override void endBatch ()
   in {
      assert (inTransaction);
   }
   body {
   }

   //
   // Non-Batch execution events
   //

   override void startExecute (StatementType statementType)
   in {
      assert (inTransaction);
   }
   body {
      /* When the statement is other than a DQL, or DML statement, the
       * transaction must be committed to ensure consistent operation.
       * Example of 'other' statements are DDL, and pragma.
       */
      if (StatementType.STATEMENT_OTHER == statementType) {
         m_commitStmt.exec ();
      }
   }

   override void executionError ()
   out {
      assert (inTransaction);
   }
   body {
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   override void endExecute ()
   out {
      assert (inTransaction);
   }
   body {
      /* When the statement is other than a DQL, or DML statement, the
       * startExecute would have committed the transaction before the
       * statement is executed. We must restore the transaction if that has
       * happened.
       */
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   //
   // A database operation was aborted. Transaction may have been terminated.
   //

   override void onAbort () {
      if (!inTransaction) {
         m_beginStmt.exec ();
      }
   }

   //
   // Explicit termination of transaction events
   //

   override bool commit ()
   in {
      assert (inTransaction);
   }
   out {
      assert (inTransaction);
   }
   body {
      bool rc = m_commitStmt.exec ();
      if (rc) {
         rc = m_beginStmt.exec ();
         assert (rc == true);
      }
      return rc;
   }

   override void rollback ()
   in {
      assert (inTransaction);
   }
   out {
      assert (inTransaction);
   }
   body {
      bool rc = m_rollbackStmt.exec ();
      assert (rc == true);
      rc = m_beginStmt.exec ();
      assert (rc == true);
   }

   override void onClose ()
   in {
      assert (inTransaction);
   }
   out {
      assert (!inTransaction);
   }
   body {
      m_rollbackStmt.exec ();
   }

   //
   // Transaction state
   //

   override bool inTransaction () {
      return inTransactionImpl (m_dbcon);
   }

   //
   //
   //

   override TransactionMode transactionMode () {
      return m_transactionMode;
   }

   override TransactionType transactionType () {
      return m_transactionType;
   }



   protected sqlite3*               m_dbcon;
   protected TransactionStatement   m_commitStmt;
   protected TransactionStatement   m_beginStmt;
   protected TransactionStatement   m_rollbackStmt;
   protected TransactionMode        m_transactionMode;
   protected TransactionType        m_transactionType;
}


//------------------------------------------------------------------------------
// Module static variables and initialisation
//------------------------------------------------------------------------------

static TransactionModeHandlerFactory   standardModeHandlerFactory;

static this() {
   standardModeHandlerFactory = new StdTrMoHaFactory ();
}

static ~this() {
   delete standardModeHandlerFactory;
}





/+

When the transaction mode is AUTO_COMMIT:

  * Transaction type is ignored.
  * Creating statements with transaction QL (BEGIN,...) are invalid.
  * Commit/Rollback does nothing and the statements created but for use by
    execytemany.
  * When ROLLBACK resolution conflict is enacted, the interface does nothing.
  * When a DDL statement is created, the interface does nothing.
  * For executemany, a transaction is created, all the iterations are executed
    and then the transaction is committed and the method ends.

When the transaction mode is USER:

  * Transaction type is ignored.
  * Creating statements with transaction QL (BEGIN,...) are allowed.
  * Commit/Rollback does nothing and the statements are not created.
  * When ROLLBACK resolution conflict is enacted, the interface does nothing.
  * When a DDL statement is created, the interface does nothing.
  * With executemany, the interface does not perform any transaction logic.

When the transaction mode is ON_MODIFY:

  * When transaction type is :
    - DEFAULT   : "BEGIN"
    - DEFERRED  : "BEGIN DEFERRED"
    - IMMEDIATE : "BEGIN IMMEDIATE"
    - EXCLUSIVE : "BEGIN EXCLUSIVE"
  * Creating statements with transaction QL (BEGIN,...) are invalid.
  * The transaction is created when a STATEMENT_MODIFY statement is executed and
    a transaction is not currently open.
  * When ROLLBACK conflict resolution is triggered, the interface does nothing.
  * When a DDL statement is executed, the interface commits an open transaction
    before the statement is executed. A tranasction is not started.
  * For executemany, if no transaction was started, then a new transaction is
    started. On completion of the method, the transaction is not committed or
    rolled back. That is for the user to do.

When the transaction mode is ALWAYS:

  * When transaction type is as per ON_MODIFY.
  * Creating statements with transaction QL (BEGIN,...) are invalid.
  * The transaction is created at the start of the connection.
  * When ROLLBACK conflict resolution is triggered, the interface creates a new
    transaction.
  * When a DDL statement is executed, the interface commits an open transaction
    before the statement is executed and then creates a new transaction.
  * For executemany, only if the conflict resolution causes a rollback will
    transaction logic be performed. In that case, a new transaction is created.
+/

